package org.jflame.commons.file;

import org.jflame.commons.codec.Transcoder;
import org.jflame.commons.model.Chars;
import org.jflame.commons.util.IOHelper;
import org.jflame.commons.util.StringHelper;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.List;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Properties文件载入工具类. 可载入多个properties文件. 相同的属性在最后载入的文件中的值将会覆盖之前的值
 * 
 * @author charles.zhang
 */
public final class PropertiesHelper {

    private static final Logger log = LoggerFactory.getLogger(PropertiesHelper.class);
    private static final Pattern varPattern = Pattern.compile("\\$\\{([^\\}]+)\\}");
    private final Properties properties = new Properties();

    /**
     * 构造函数.
     * <p>
     * 文件路径以"classpath:"开头或是相对路径则从当前classpath查找,否则作为绝对路径.示例:<br>
     * 相对路径:"classpath:jdbc.properties","config/jdbc.properties";<br>
     * 绝对路径:"d:/jdbc.properties","/home/user/jdbc.properties";
     * </p>
     * 
     * @param resourcesPaths 资源文件路径
     * @throws IOException
     */
    public PropertiesHelper(String... resourcesPaths) throws IOException {
        loadProperties(resourcesPaths);
    }

    public Properties getProperties() {
        return properties;
    }

    /**
     * 取出属性值，作为字符串返回.
     * 
     * @param key 属性名
     */
    private String getValue(String key) {
        return properties.getProperty(key);
    }

    /**
     * 取出String类型的属性
     * 
     * @param key 属性名
     */
    public String getProperty(String key) {
        String value = getValue(key);
        return value != null ? value.trim() : value;
    }

    /**
     * 取出String类型的属性
     * 
     * @param key 属性名
     * @param defaultValue 默认值
     */
    public String getProperty(String key, String defaultValue) {
        String value = getValue(key);
        return value != null ? value.trim() : defaultValue;
    }

    public void setProperty(String key, String value) {
        properties.setProperty(key, value);
    }

    /**
     * 查找指定key的属性是否存在
     * 
     * @param key 属性名
     * @return
     */
    public boolean hasProperty(String key) {
        return properties.containsKey(key);
    }

    /**
     * 读取多个资源文件.
     * <p>
     * 文件路径以"classpath:"开头或是相对路径则从当前classpath查找,否则作为绝对路径.示例:<br/>
     * 相对路径:"classpath:jdbc.properties","config/jdbc.properties"<br/>
     * 绝对路径:"d:/jdbc.properties","/home/user/jdbc.properties"
     * </p>
     * 
     * @param resourcesPaths 资源文件路径
     * @throws IOException
     */
    private void loadProperties(String... resourcesPaths) throws IOException {
        for (String location : resourcesPaths) {
            if (log.isDebugEnabled()) {
                log.debug("加载资源文件{}", location);
            }
            InputStream inStream = null;
            location = location.replaceFirst("classpath:/?", "");

            // 非绝对路径从classpath读取
            if (isAbsolute(location)) {
                inStream = new FileInputStream(location);
            } else {
                inStream = FileHelper.readFileFromClassPath(location);
            }
            try {
                if (inStream != null) {
                    properties.load(inStream);
                }
            } catch (IOException ex) {
                log.error("加载资源文件失败" + location, ex);
                throw ex;
            } finally {
                IOHelper.closeQuietly(inStream);
            }

        }
        // 替换变量${}
        if (!properties.isEmpty()) {
            StringBuffer buffer = new StringBuffer();
            for (Entry<Object,Object> entry : properties.entrySet()) {
                String value = properties.getProperty((String) entry.getKey());
                Matcher matcher = varPattern.matcher(value);
                buffer.setLength(0);
                while (matcher.find()) {
                    String matcherKey = matcher.group(1);
                    String matchervalue = properties.getProperty(matcherKey);
                    // 找系统环境变量
                    if (matchervalue == null) {
                        matchervalue = System.getProperty(matcherKey);
                    }
                    if (matchervalue != null) {
                        // 替换特殊字符\$
                        if (StringHelper.containsAny(matchervalue, '\\', '$')) {
                            matcher.appendReplacement(buffer, matchervalue.replaceAll("\\\\", "\\\\\\\\")
                                    .replaceAll("\\$", "\\\\\\$"));
                        } else {
                            matcher.appendReplacement(buffer, matchervalue);
                        }
                    }
                }
                matcher.appendTail(buffer);
                properties.put(entry.getKey(), buffer.toString());
            }
        } else {
            log.warn("未加载到任何属性");
        }
    }

    public void store(Path filePath) throws IOException {
        try (OutputStream out = Files.newOutputStream(filePath)) {
            properties.store(out, null);
        }
    }

    /**
     * 更新properties文件,只更新值,保留原格式(键顺序和注释)
     * 
     * @param propertiesPath
     * @param newProperties
     * @throws IOException
     */
    public static void updateOnlyValue(Path propertiesPath, Properties newProperties) throws IOException {
        List<String> allline = Files.readAllLines(propertiesPath);
        for (int i = 0; i < allline.size(); i++) {
            String line = allline.get(i);
            if (StringUtils.isWhitespace(line)) {
                continue;
            }
            String tmpLine = line.trim();
            if (tmpLine.charAt(0) == '#') {
                continue;
            }
            String key = StringUtils.substringBefore(tmpLine, "=");
            if (newProperties.containsKey(key)) {
                tmpLine = key + "=" + saveConvert(newProperties.getProperty(key), false, true);
                allline.set(i, tmpLine);
            }
        }
        Files.write(propertiesPath, allline, StandardCharsets.ISO_8859_1, StandardOpenOption.TRUNCATE_EXISTING);
    }

    private static String saveConvert(String theString, boolean escapeSpace, boolean escapeUnicode) {
        if (theString == null || StringUtils.isWhitespace(theString)) {
            return "";
        }
        int len = theString.length();
        int bufLen = len * 2;
        if (bufLen < 0) {
            bufLen = Integer.MAX_VALUE;
        }
        StringBuilder outBuffer = new StringBuilder(bufLen);

        for (int x = 0; x < len; x++) {
            char aChar = theString.charAt(x);
            // Handle common case first, selecting largest block that
            // avoids the specials below
            if ((aChar > 61) && (aChar < 127)) {
                if (aChar == '\\') {
                    outBuffer.append('\\');
                    outBuffer.append('\\');
                    continue;
                }
                outBuffer.append(aChar);
                continue;
            }
            switch (aChar) {
                case ' ':
                    if (x == 0 || escapeSpace)
                        outBuffer.append('\\');
                    outBuffer.append(' ');
                    break;
                case '\t':
                    outBuffer.append('\\');
                    outBuffer.append('t');
                    break;
                case '\n':
                    outBuffer.append('\\');
                    outBuffer.append('n');
                    break;
                case '\r':
                    outBuffer.append('\\');
                    outBuffer.append('r');
                    break;
                case '\f':
                    outBuffer.append('\\');
                    outBuffer.append('f');
                    break;
                case '=': // Fall through
                case ':': // Fall through
                case '#': // Fall through
                case '!':
                    outBuffer.append('\\');
                    outBuffer.append(aChar);
                    break;
                default:
                    if (((aChar < 0x0020) || (aChar > 0x007e)) & escapeUnicode) {
                        /*outBuffer.append('\\');
                        outBuffer.append('u');
                        outBuffer.append(toHex((aChar >> 12) & 0xF));
                        outBuffer.append(toHex((aChar >> 8) & 0xF));
                        outBuffer.append(toHex((aChar >> 4) & 0xF));
                        outBuffer.append(toHex(aChar & 0xF));*/
                        outBuffer.append(Transcoder.encodeUnicode(aChar));
                    } else {
                        outBuffer.append(aChar);
                    }
            }
        }
        return outBuffer.toString();
    }

    /*private static char toHex(int nibble) {
        return Hex.HEX_CHARS[(nibble & 0xF)];
    }*/

    boolean isAbsolute(String path) {
        Path p = Paths.get(path);
        if (p.isAbsolute()) {
            return true;
        }
        // 非windows系统,/开头视为绝对路径
        if (!SystemUtils.IS_OS_WINDOWS && path.charAt(0) == Chars.SLASH) {
            return true;
        }
        return false;
    }

}
